package router

import (
	"context"
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"strings"

	"gitee.com/zengxinqian/hall"
	"gitee.com/zengxinqian/hall/router/radix"
	"gitee.com/zengxinqian/hall/router/util/bytebufferpool"
)

/*
Router functions
*/

// MethodWild wild HTTP method
const MethodWild = "*"

var (
	questionMark = byte('?')

	// MatchedRoutePathParam is the param name under which the path of the matched
	// route is stored, if Router.SaveMatchedRoutePath is set.
	MatchedRoutePathParam = fmt.Sprintf("__matchedRoutePath::%s__", bytesRand(make([]byte, 15)))
)

// NewRouter New returns a new router.
// Path autocorrection, including trailing slashes, is enabled by default.
func NewRouter[Req any, Resp any]() *Router[Req, Resp] {

	return &Router[Req, Resp]{
		trees:                  make([]*radix.Tree[Req, Resp], 10),
		customMethodsIndex:     make(map[string]int),
		registeredPaths:        make(map[string][]string),
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      true,
		HandleMethodNotAllowed: true,
		HandleOPTIONS:          true,
	}

}

// Group returns a new group.
// Path autocorrection, including trailing slashes, is enabled by default.
func (r *Router[Req, Resp]) Group(path string) *Group[Req, Resp] {

	validatePath(path)

	if path != "/" && strings.HasSuffix(path, "/") {
		panic("group path must not end with a trailing slash")
	}

	return &Group[Req, Resp]{
		router: r,
		prefix: path,
	}

}

func (r *Router[Req, Resp]) saveMatchedRoutePath(path string, handler hall.Handler[Req, Resp]) hall.Handler[Req, Resp] {

	return &hall.FuncHandler[Req, Resp]{
		Decoder: handler.ReqDecoder,
		Encoder: handler.RespEncoder,
		HandlerFunc: func(ctx context.Context, req Req, resp *Resp) {
			params := hall.UserParamsFromContext(ctx)
			params.SetUserValue(MatchedRoutePathParam, path)
			handler.Handle(ctx, req, resp)
		},
	}

}

func (r *Router[Req, Resp]) methodIndexOf(method string) int {

	switch method {
	case hall.MethodGet:
		return 0
	case hall.MethodHead:
		return 1
	case hall.MethodPost:
		return 2
	case hall.MethodPut:
		return 3
	case hall.MethodPatch:
		return 4
	case hall.MethodDelete:
		return 5
	case hall.MethodConnect:
		return 6
	case hall.MethodOptions:
		return 7
	case hall.MethodTrace:
		return 8
	case MethodWild:
		return 9
	}

	if i, ok := r.customMethodsIndex[method]; ok {
		return i
	}

	return -1

}

// Mutable allows updating the route handler
//
// # It's disabled by default
//
// WARNING: Use with care. It could generate unexpected behaviours
func (r *Router[Req, Resp]) Mutable(v bool) {

	r.treeMutable = v

	for i := range r.trees {
		tree := r.trees[i]

		if tree != nil {
			tree.Mutable = v
		}
	}

}

// List returns all registered routes grouped by method
func (r *Router[Req, Resp]) List() map[string][]string {
	return r.registeredPaths
}

// GET is a shortcut for router.Handle(hall.MethodGet, path, handler)
func (r *Router[Req, Resp]) GET(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodGet, path, handler)
}

// HEAD is a shortcut for router.Handle(hall.MethodHead, path, handler)
func (r *Router[Req, Resp]) HEAD(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodHead, path, handler)
}

// POST is a shortcut for router.Handle(hall.MethodPost, path, handler)
func (r *Router[Req, Resp]) POST(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodPost, path, handler)
}

// PUT is a shortcut for router.Handle(hall.MethodPut, path, handler)
func (r *Router[Req, Resp]) PUT(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodPut, path, handler)
}

// PATCH is a shortcut for router.Handle(hall.MethodPatch, path, handler)
func (r *Router[Req, Resp]) PATCH(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodPatch, path, handler)
}

// DELETE is a shortcut for router.Handle(hall.MethodDelete, path, handler)
func (r *Router[Req, Resp]) DELETE(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodDelete, path, handler)
}

// CONNECT is a shortcut for router.Handle(hall.MethodConnect, path, handler)
func (r *Router[Req, Resp]) CONNECT(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodConnect, path, handler)
}

// OPTIONS is a shortcut for router.Handle(hall.MethodOptions, path, handler)
func (r *Router[Req, Resp]) OPTIONS(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodOptions, path, handler)
}

// TRACE is a shortcut for router.Handle(hall.MethodTrace, path, handler)
func (r *Router[Req, Resp]) TRACE(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(hall.MethodTrace, path, handler)
}

// ANY is a shortcut for router.Handle(router.MethodWild, path, handler)
//
// WARNING: Use only for routes where the request method is not important
func (r *Router[Req, Resp]) ANY(path string, handler hall.Handler[Req, Resp]) {
	r.Handle(MethodWild, path, handler)
}

// ServeFiles serves files from the given file system root path.
// The path must end with "/{filepath:*}", files are then served from the local
// path /defined/root/dir/{filepath:*}.
// For example if root is "/etc" and {filepath:*} is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a fasthttp.FSHandler is used, therefore fasthttp.NotFound is used instead
// Use:
//
//	router.ServeFiles("/src/{filepath:*}", "./")
func (r *Router[Req, Resp]) ServeFiles(path string, rootPath string) {
	//r.ServeFilesCustom(path, &fasthttp.FS{
	//	Root:               rootPath,
	//	IndexNames:         []string{"index.html"},
	//	GenerateIndexPages: true,
	//	AcceptByteRange:    true,
	//})
}

// ServeFS serves files from the given file system.
// The path must end with "/{filepath:*}", files are then served from the local
// path /defined/root/dir/{filepath:*}.
// For example if root is "/etc" and {filepath:*} is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a fasthttp.FSHandler is used, therefore fasthttp.NotFound is used instead
// Use:
//
//	router.ServeFS("/src/{filepath:*}", myFilesystem)
func (r *Router[Req, Resp]) ServeFS(path string, filesystem fs.FS) {
	//r.ServeFilesCustom(path, &fasthttp.FS{
	//	FS:                 filesystem,
	//	Root:               "",
	//	AllowEmptyRoot:     true,
	//	GenerateIndexPages: true,
	//	AcceptByteRange:    true,
	//	Compress:           true,
	//	CompressBrotli:     true,
	//})
}

// ServeFilesCustom serves files from the given file system settings.
// The path must end with "/{filepath:*}", files are then served from the local
// path /defined/root/dir/{filepath:*}.
// For example if root is "/etc" and {filepath:*} is "passwd", the local file
// "/etc/passwd" would be served.
// Internally a fasthttp.FSHandler is used, therefore http.NotFound is used instead
// of the Router's NotFound handler.
// Use:
//
//	router.ServeFilesCustom("/src/{filepath:*}", *customFS)
func (r *Router[Req, Resp]) ServeFilesCustom(path string, fs fs.FS) {

	//const suffix = "/{filepath:*}"
	//
	//if !strings.HasSuffix(path, suffix) {
	//	panic("path must end with " + suffix + " in path '" + path + "'")
	//}
	//
	//prefix := path[:len(path)-len(suffix)]
	//stripSlashes := strings.Count(prefix, "/")
	//
	//if fs.PathRewrite == nil && stripSlashes > 0 {
	//	fs.PathRewrite = fasthttp.NewPathSlashesStripper(stripSlashes)
	//}
	//fileHandler := fs.NewRequestHandler()
	//
	//r.GET(path, fileHandler)

}

// Handle registers a new request handler with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
// frequently used, non-standardized or custom methods (e.g. for internal
// communication with a proxy).
func (r *Router[Req, Resp]) Handle(method, path string, handler hall.Handler[Req, Resp]) {

	switch {
	case len(method) == 0:
		panic("method must not be empty")
	case handler == nil:
		panic("handler must not be nil")
	default:
		validatePath(path)
	}

	r.registeredPaths[method] = append(r.registeredPaths[method], path)

	methodIndex := r.methodIndexOf(method)
	if methodIndex == -1 {
		tree := radix.New[Req, Resp]()
		tree.Mutable = r.treeMutable

		r.trees = append(r.trees, tree)
		methodIndex = len(r.trees) - 1
		r.customMethodsIndex[method] = methodIndex
	}

	tree := r.trees[methodIndex]
	if tree == nil {
		tree = radix.New[Req, Resp]()
		tree.Mutable = r.treeMutable

		r.trees[methodIndex] = tree
		r.globalAllowed = r.allowed("*", "", nil)
	}

	if r.SaveMatchedRoutePath {
		handler = r.saveMatchedRoutePath(path, handler)
	}

	optionalPaths := getOptionalPaths(path)

	// if not has optional paths, adds the original
	if len(optionalPaths) == 0 {
		tree.Add(path, handler)
	} else {
		for _, p := range optionalPaths {
			tree.Add(p, handler)
		}
	}

}

// Lookup allows the manual lookup of a method + path combo.
// This is e.g. useful to build a framework around this router.
// If the path was found, it returns the handler function.
// Otherwise, the second return value indicates whether a redirection to
// the same path with an extra / without the trailing slash should be performed.
func (r *Router[Req, Resp]) Lookup(method, path string, ctx context.Context) (hall.Handler[Req, Resp], bool) {

	methodIndex := r.methodIndexOf(method)
	if methodIndex == -1 {
		return nil, false
	}

	if tree := r.trees[methodIndex]; tree != nil {
		handler, tsr := tree.Get(path, ctx)
		if handler != nil || tsr {
			return handler, tsr
		}
	}

	if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil {
		return tree.Get(path, ctx)
	}

	return nil, false

}

func (r *Router[Req, Resp]) recv(ctx context.Context) {
	if rcv := recover(); rcv != nil {
		r.PanicHandler(ctx, rcv)
	}
}

func (r *Router[Req, Resp]) allowed(path, reqMethod string, ctx context.Context) (allow string) {

	allowed := make([]string, 0, 9)

	if path == "*" || path == "/*" { // server-wide{ // server-wide
		// empty method is used for internal calls to refresh the cache
		if reqMethod == "" {
			for method := range r.registeredPaths {
				if method == hall.MethodOptions {
					continue
				}
				// Add request method to list of allowed methods
				allowed = append(allowed, method)
			}
		} else {
			return r.globalAllowed
		}
	} else { // specific path
		for method := range r.registeredPaths {
			// Skip the requested method - we already tried this one
			if method == reqMethod || method == hall.MethodOptions {
				continue
			}

			handle, _ := r.trees[r.methodIndexOf(method)].Get(path, ctx)
			if handle != nil {
				// Add request method to list of allowed methods
				allowed = append(allowed, method)
			}
		}
	}

	if len(allowed) > 0 {
		// Add request method to list of allowed methods
		allowed = append(allowed, hall.MethodOptions)

		// Sort allowed methods.
		// sort.Strings(allowed) unfortunately causes unnecessary allocations
		// due to allowed being moved to the heap and interface conversion
		for i, l := 1, len(allowed); i < l; i++ {
			for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- {
				allowed[j], allowed[j-1] = allowed[j-1], allowed[j]
			}
		}

		// return as comma separated list
		return strings.Join(allowed, ", ")
	}
	return

}

func (r *Router[Req, Resp]) tryRedirect(writer http.ResponseWriter, request *http.Request, tree *radix.Tree[Req, Resp], tsr bool, method, path string) bool {

	// Moved Permanently, request with GET method
	code := hall.StatusMovedPermanently
	if method != hall.MethodGet {
		// Permanent Redirect, request with same method
		code = hall.StatusPermanentRedirect
	}

	if tsr && r.RedirectTrailingSlash {

		uri := bytebufferpool.Get()

		if len(path) > 1 && path[len(path)-1] == '/' {
			uri.SetString(path[:len(path)-1])
		} else {
			uri.SetString(path)
			_ = uri.WriteByte('/')
		}

		if queryBuf := request.URL.RawQuery; len(queryBuf) > 0 {
			_ = uri.WriteByte(questionMark)
			_, _ = uri.Write([]byte(queryBuf))
		}

		http.Redirect(writer, request, uri.String(), code)
		bytebufferpool.Put(uri)

		return true

	}

	// Try to fix the request path
	if r.RedirectFixedPath {
		path2 := request.URL.Path

		uri := bytebufferpool.Get()
		found := tree.FindCaseInsensitivePath(
			cleanPath(path2),
			r.RedirectTrailingSlash,
			uri,
		)

		if found {
			if queryBuf := request.URL.RawQuery; len(queryBuf) > 0 {
				_ = uri.WriteByte(questionMark)
				_, _ = uri.Write([]byte(queryBuf))
			}

			http.Redirect(writer, request, uri.String(), code)
			bytebufferpool.Put(uri)

			return true
		}

		bytebufferpool.Put(uri)
	}

	return false
}

// Handler makes the router implement the http.Handler interface.
func (r *Router[Req, Resp]) ServeHTTP(writer http.ResponseWriter, request *http.Request) {

	var req Req
	var resp Resp

	if r.PanicHandler != nil {
		defer r.recv(request.Context())
	}

	ctx := context.WithValue(request.Context(), hall.RequestKey, request)
	ctx = context.WithValue(ctx, hall.UserParamsKey, hall.NewUserParams())

	path := request.URL.Path
	method := request.Method
	methodIndex := r.methodIndexOf(method)

	var processHandler = func(handler hall.Handler[Req, Resp]) {

		err := handler.ReqDecoder(ctx, request.Body, &req)
		if err != nil {
			writer.WriteHeader(http.StatusBadRequest)
			_, _ = writer.Write([]byte(err.Error()))
			return
		}

		handler.Handle(ctx, req, &resp)
		err = handler.RespEncoder(ctx, resp, writer)
		if err != nil {
			writer.WriteHeader(http.StatusInternalServerError)
		}

	}

	if methodIndex > -1 {
		if tree := r.trees[methodIndex]; tree != nil {
			if handler, tsr := tree.Get(path, ctx); handler != nil {
				processHandler(handler)
				return
			} else if method != hall.MethodConnect && path != "/" {
				if ok := r.tryRedirect(writer, request, tree, tsr, method, path); ok {
					return
				}
			}
		}
	}

	// Try to search in the wild method tree
	if tree := r.trees[r.methodIndexOf(MethodWild)]; tree != nil {
		if handler, tsr := tree.Get(path, ctx); handler != nil {
			processHandler(handler)
			return
		} else if method != hall.MethodConnect && path != "/" {
			if ok := r.tryRedirect(writer, request, tree, tsr, method, path); ok {
				return
			}
		}
	}

	if r.HandleOPTIONS && method == hall.MethodOptions {
		// Handle OPTIONS requests

		if allow := r.allowed(path, hall.MethodOptions, ctx); allow != "" {
			writer.Header().Set("Allow", allow)
			if r.GlobalOPTIONS != nil {
				processHandler(r.GlobalOPTIONS)
			}
			return
		}
	} else if r.HandleMethodNotAllowed {
		// Handle 405
		if allow := r.allowed(path, method, ctx); allow != "" {
			writer.Header().Set("Allow", allow)
			if r.MethodNotAllowed != nil {
				processHandler(r.MethodNotAllowed)
			} else {
				writer.WriteHeader(http.StatusMethodNotAllowed)
				_, _ = writer.Write([]byte(http.StatusText(http.StatusMethodNotAllowed)))
			}
			return
		}
	}

	// Handle 404
	if r.NotFound != nil {
		processHandler(r.NotFound)
	} else {
		writer.WriteHeader(http.StatusNotFound)
		_, _ = writer.Write([]byte(http.StatusText(http.StatusNotFound)))
	}

}

func (r *Router[Req, Resp]) SetDecoder(decoder hall.ReqDecoder[Req]) {
	r.decoder = decoder
}

func (r *Router[Req, Resp]) SetEncoder(encoder hall.RespEncoder[Resp]) {
	r.encoder = encoder
}

func (r *Router[Req, Resp]) Wrap(handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) *hall.FuncHandler[Req, Resp] {

	defaultDecoder := func(ctx context.Context, reader io.Reader, req *Req) error {
		if r.decoder != nil {
			return r.decoder(ctx, reader, req)
		}
		return hall.AutoDecoder(ctx, reader, req)
	}

	defaultEncoder := func(ctx context.Context, resp Resp, writer io.Writer) error {
		if r.encoder != nil {
			return r.encoder(ctx, resp, writer)
		}
		return hall.AutoEncoder(ctx, resp, writer)
	}

	h := &hall.FuncHandler[Req, Resp]{
		HandlerFunc: handlerFunc,
		Decoder:     defaultDecoder,
		Encoder:     defaultEncoder,
	}
	for _, c := range codecs {
		if decoder, ok := c.(hall.ReqDecoder[Req]); ok {
			h.Decoder = decoder
		}
		if encoder, ok := c.(hall.RespEncoder[Resp]); ok {
			h.Encoder = encoder
		}
	}
	return h

}

func (r *Router[Req, Resp]) GETFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.GET(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) POSTFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.POST(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) PUTFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.PUT(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) DELETEFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.DELETE(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) TRACEFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.TRACE(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) OPTIONSFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.OPTIONS(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) HEADFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.HEAD(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) PATCHFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.PATCH(uri, r.Wrap(handlerFunc, codecs...))
}

func (r *Router[Req, Resp]) ANYFunc(uri string, handlerFunc hall.HandlerFunc[Req, Resp], codecs ...interface{}) {
	r.ANY(uri, r.Wrap(handlerFunc, codecs...))
}
